﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="DeviceInfoSet.cs" company="Matthias Friedrich">
//   Copyright © Matthias Friedrich 2009
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace DeviceManagement
{
	using System;
	using System.Collections.Generic;
	using System.Runtime.InteropServices;
	using System.Text;

	public class DeviceInfoSet : IDisposable
	{
		private readonly DeviceInterface deviceInterface;

		private bool interfaceEnabled;

		protected internal DeviceInfoSet(IntPtr handle, DeviceInterface deviceInterface = null)
		{
			if (handle == IntPtr.Zero)
			{
				throw new ArgumentException("The given device info handle is invalid.");
			}

			this.Handle = handle;
			this.deviceInterface = deviceInterface;
			this.interfaceEnabled = this.deviceInterface != null;
		}

		public IntPtr Handle { get; private set; }

		/// <summary>
		///     Builds a list of all devices in the system, including devices that are not currently present.
		/// </summary>
		public static DeviceInfoSet GetAllClasses()
		{
			IntPtr handle = DeviceInstallation.SetupDiGetClassDevs(IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, DeviceInstallation.DIGCF_ALLCLASSES);

			return new DeviceInfoSet(handle);
		}

		/// <summary>
		///     Builds a list of all devices that are present in the system.
		/// </summary>
		public static DeviceInfoSet GetAllClassesPresent()
		{
			IntPtr handle = DeviceInstallation.SetupDiGetClassDevs(IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, DeviceInstallation.DIGCF_ALLCLASSES | DeviceInstallation.DIGCF_PRESENT);

			return new DeviceInfoSet(handle);
		}

		/// <summary>
		/// Retrieves the class description associated with the specified setup class GUID.
		/// </summary>
		/// <param name="classGuid">
		/// The class Guid.
		/// </param>
		public static string GetClassDescription(Guid classGuid)
		{
			uint requiredSize = 0;
			DeviceInstallation.SetupDiGetClassDescription(ref classGuid, IntPtr.Zero, 0, ref requiredSize);

			uint size = requiredSize;
			var buffer = new StringBuilder((int)size);
			if (DeviceInstallation.SetupDiGetClassDescription(ref classGuid, buffer, size, ref requiredSize))
			{
				return buffer.ToString();
			}

			return null;
		}

		public static IntPtr GetClassIcon(Guid classGuid)
		{
			IntPtr pIcon;
			int miniIconIndex = 0;
			DeviceInstallation.SetupDiLoadClassIcon(ref classGuid, out pIcon, ref miniIconIndex);
			return pIcon;
		}

		/// <summary>
		/// Retrieves the class name associated with a class GUID.
		/// </summary>
		/// <param name="classGuid">
		/// The class Guid.
		/// </param>
		public static string GetClassName(Guid classGuid)
		{
			uint requiredSize = 0;
			DeviceInstallation.SetupDiClassNameFromGuid(ref classGuid, IntPtr.Zero, 0, ref requiredSize);

			uint size = requiredSize;
			var buffer = new StringBuilder((int)size);
			if (DeviceInstallation.SetupDiClassNameFromGuid(ref classGuid, buffer, size, ref requiredSize))
			{
				return buffer.ToString();
			}

			return null;
		}

		/// <summary>
		/// Builds a list of all devices that are present in the system that have enabled an interface from the given device interface.
		/// </summary>
		/// <param name="deviceInterface">
		/// The device Interface.
		/// </param>
		public static DeviceInfoSet GetPresent(DeviceInterface deviceInterface)
		{
			if (deviceInterface == null)
			{
				throw new ArgumentNullException("deviceInterface");
			}

			Guid classGuid = deviceInterface.ClassGuid;

			IntPtr handle = DeviceInstallation.SetupDiGetClassDevs(ref classGuid, IntPtr.Zero, IntPtr.Zero, DeviceInstallation.DIGCF_PRESENT | DeviceInstallation.DIGCF_DEVICEINTERFACE);

			return new DeviceInfoSet(handle, deviceInterface);
		}

		public IEnumerable<DeviceInterfaceData> GetDeviceInterfaceData(DeviceInfo deviceInfo)
		{
			if (deviceInfo == null)
			{
				throw new ArgumentNullException("deviceInfo");
			}

			if (!this.HasInterface())
			{
				return new List<DeviceInterfaceData>().AsReadOnly();
			}

			SP_DEVINFO_DATA deviceInfoData = deviceInfo.DeviceInfoData;

			Guid guid = this.deviceInterface.ClassGuid;

			return this.GetDeviceInterfaceDataInternal(deviceInfoData, guid);
		}

		public IEnumerable<DeviceInfo> GetDevices()
		{
			uint index = 0;

			var results = new List<DeviceInfo>();
			while (true)
			{
				var deviceInfoData = new SP_DEVINFO_DATA();
				deviceInfoData.cbSize = (uint)Marshal.SizeOf(typeof(SP_DEVINFO_DATA));
				bool result = DeviceInstallation.SetupDiEnumDeviceInfo(this.Handle, index++, ref deviceInfoData);

				if (result)
				{
					results.Add(new DeviceInfo(this.Handle, deviceInfoData));
				}
				else
				{
					ThrowDeviceInstallationExceptionFromLastError();
					break;
				}
			}

			return results.AsReadOnly();
		}

		public bool HasInterface()
		{
			return this.deviceInterface != null;
		}

		private static void ThrowDeviceInstallationExceptionFromLastError()
		{
			uint e = Kernel32.GetLastError();
			if (e != Kernel32.ERROR_SUCCESS)
			{
				string errorMessage = Kernel32.GetLastErrorMessage();
				if (e != DeviceInstallation.ERROR_NO_MORE_ITEMS)
				{
					throw new DeviceInstallationException(errorMessage);
				}
			}
		}

		private IEnumerable<DeviceInterfaceData> GetDeviceInterfaceDataInternal(SP_DEVINFO_DATA deviceInfoData, Guid classGuid)
		{
			uint index = 0;

			var results = new List<DeviceInterfaceData>();

			while (true)
			{
				var deviceInterfaceData = new SP_DEVICE_INTERFACE_DATA();
				deviceInterfaceData.cbSize = (uint)Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DATA));
				bool result = DeviceInstallation.SetupDiEnumDeviceInterfaces(this.Handle, ref deviceInfoData, ref classGuid, index++, ref deviceInterfaceData);

				if (result)
				{
					SP_DEVICE_INTERFACE_DETAIL_DATA deviceInterfaceDetailData = this.GetDeviceInterfaceDetailData(deviceInfoData, deviceInterfaceData);
					results.Add(new DeviceInterfaceData(deviceInterfaceData, deviceInterfaceDetailData));
				}
				else
				{
					ThrowDeviceInstallationExceptionFromLastError();
					break;
				}
			}

			return results.AsReadOnly();
		}

		private SP_DEVICE_INTERFACE_DETAIL_DATA GetDeviceInterfaceDetailData(SP_DEVINFO_DATA deviceInfoData, SP_DEVICE_INTERFACE_DATA deviceInterfaceData)
		{
			uint requiredSize = 0;
			DeviceInstallation.SetupDiGetDeviceInterfaceDetail(this.Handle, ref deviceInterfaceData, IntPtr.Zero, 0, ref requiredSize, ref deviceInfoData);

			var deviceInterfaceDetailData = new SP_DEVICE_INTERFACE_DETAIL_DATA();

			if (IntPtr.Size == 8)
			{
				deviceInterfaceDetailData.cbSize = 8; // This did the trick for Windows 7, x64
			}
			else
			{
				deviceInterfaceDetailData.cbSize = 6; // (uint) (Marshal.SizeOf(typeof(uint)) + Marshal.SizeOf(typeof(char)));
			}

			uint deviceInterfaceDetailDataSize = requiredSize;

			if (DeviceInstallation.SetupDiGetDeviceInterfaceDetail(this.Handle, ref deviceInterfaceData, ref deviceInterfaceDetailData, 
				deviceInterfaceDetailDataSize, ref requiredSize, ref deviceInfoData))
			{
				return deviceInterfaceDetailData;
			}

			ThrowDeviceInstallationExceptionFromLastError();

			return SP_DEVICE_INTERFACE_DETAIL_DATA.Empty;
		}

		#region IDisposable Members

		public void Dispose()
		{
			this.Dispose(true);
			GC.SuppressFinalize(this);
		}

		public bool Disposed { get; private set; }

		protected virtual void Dispose(bool disposing)
		{
			if (disposing && !this.Disposed)
			{
				this.Disposed = true;
			}

			this.Release();
		}

		private void Release()
		{
			try
			{
				if (this.Handle != IntPtr.Zero)
				{
					DeviceInstallation.SetupDiDestroyDeviceInfoList(this.Handle);
				}
			}
			catch
			{
			}
		}

		~DeviceInfoSet()
		{
			this.Dispose(false);
		}

		#endregion
	}
}